package com.cloudbroker.bcs.common.datasource;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.dbcp.BasicDataSource;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.cloudbroker.bcs.common.configure.HSBCSZooKeeperConfigurator;
import com.cloudbroker.bcs.common.event.annotation.On;
import com.cloudbroker.bcs.common.util.ValueUtil;

public abstract class NameMappingDataSource extends BasicDataSource {
    
    public static final String PROP_KEY_DRIVERCLASSNAME = "driverClassName";
    
    public static final String PROP_KEY_URL = "url";
    
    public static final String PROP_KEY_USERNAME = "username";
    
    public static final String PROP_KEY_PASSWORD = "password";
    
    public static final String PROP_KEY_MAXACTIVE = "maxActive";
    
    public static final String PROP_KEY_INITIALSIZE = "initialSize";
    
    public static final String PROP_KEY_MAXWAIT = "maxWait";
    
    public static final String PROP_KEY_MAXIDLE = "maxIdle";
    
    public static final String PROP_KEY_MINIDLE = "minIdle";
    
    public static final String PROP_KEY_REMOVEABANDONED = "removeAbandoned";
    
    public static final String PROP_KEY_REMOVEABANDONEDTIMEOUT = "removeAbandonedTimeout";
    
    public static final String PROP_KEY_TIMEBETWEENEVICTIONRUNSMILLS = "timeBetweenEvictionRunsMillis";
    
    public static final String PROP_KEY_MINEVICTABLEIDLETIMEMILLIS = "minEvictableIdleTimeMillis";
    
    public static final String PROP_KEY_CONNECTIONPROPERTIES = "connectionProperties";
    
    private static final Logger LOG = LoggerFactory.getLogger(NameMappingDataSource.class);
    
    protected boolean removeAbandoned;
    
    protected int removeAbandonedTimeout;
    
    protected String connProperties;
    
    protected Map<String, Map<String, Object>> dsPropsMap;
    
    protected Map<String, BasicDataSource> dsMap = new HashMap<String, BasicDataSource>();
    
    protected Lock dsMapLock = new ReentrantLock();
    
    public BasicDataSource getDataSource(String sourceName) {
        BasicDataSource ds = dsMap.get(sourceName);
        if (null == ds) {
            dsMapLock.lock();
            try {
                ds = dsMap.get(sourceName);
                if (null == ds) {
                    Map<String, Object> propsMap = getPropsMap(sourceName);
                    ds = createDataSource(sourceName, propsMap);
                    dsMap.put(sourceName, ds);
                }
            } finally {
                dsMapLock.unlock();
            }
        }
        return ds;
    }
    
    protected Map<String, Object> getPropsMap(String sourceName) {
        Map<String, Object> propsMap = null;
        if (null != dsPropsMap) {
            propsMap = dsPropsMap.get(sourceName);
            if (null == propsMap) {
                throw new RuntimeException("undefined sourceName " + sourceName);
            }
        }
        return propsMap;
    }
    
    private BasicDataSource createDataSource(String sourceName, Map<String, Object> propsMap) {
        BasicDataSource dataSource = new BasicDataSource();
        
        // driverClassName
        String driverClassName = ValueUtil.getString(propsMap.get(PROP_KEY_DRIVERCLASSNAME), null);
        if (StringUtils.isBlank(driverClassName)) {
            driverClassName = this.driverClassName;
        }
        dataSource.setDriverClassName(driverClassName);
        
        // url
        String url = ValueUtil.getString(propsMap.get(PROP_KEY_URL), null);
        if (StringUtils.isBlank(url)) {
            throw new RuntimeException(PROP_KEY_URL + " for " + sourceName + " is required");
        }
        dataSource.setUrl(url);
        
        // username
        String username = ValueUtil.getString(propsMap.get(PROP_KEY_USERNAME), null);
        if (StringUtils.isBlank(username)) {
            throw new RuntimeException(PROP_KEY_USERNAME + " for " + sourceName + " is required");
        }
        dataSource.setUsername(username);
        
        // password
        String password = ValueUtil.getString(propsMap.get(PROP_KEY_PASSWORD), null);
        if (null == password) {
            throw new RuntimeException(PROP_KEY_PASSWORD + " for " + sourceName + " is required");
        }
        dataSource.setPassword(password);
        
        // initialSize
        int initialSize = ValueUtil.getInt(propsMap.get(PROP_KEY_INITIALSIZE), -1);
        if (-1 == initialSize) {
            initialSize = this.initialSize;
        }
        dataSource.setInitialSize(initialSize);
        
        // maxActive
        int maxActive = ValueUtil.getInt(propsMap.get(PROP_KEY_MAXACTIVE), -1);
        if (-1 == maxActive) {
            maxActive = this.maxActive;
        }
        dataSource.setMaxActive(maxActive);
        
        // maxIdle
        int maxIdle = ValueUtil.getInt(propsMap.get(PROP_KEY_MAXIDLE), -1);
        if (-1 == maxIdle) {
            maxIdle = this.maxIdle;
        }
        dataSource.setMaxIdle(maxIdle);
        
        // minIdle
        int minIdle = ValueUtil.getInt(propsMap.get(PROP_KEY_MINIDLE), -1);
        if (-1 == minIdle) {
            minIdle = this.minIdle;
        }
        dataSource.setMinIdle(minIdle);
        
        // maxWait
        long maxWait = ValueUtil.getLong(propsMap.get(PROP_KEY_MAXWAIT), -1);
        if (-1 == maxWait) {
            maxWait = this.maxWait;
        }
        dataSource.setMaxWait(maxWait);
        
        // removeAbandoned
        Boolean removeAbandoned = ValueUtil.getBooleanObj(propsMap.get(PROP_KEY_REMOVEABANDONED));
        if (null == removeAbandoned) {
            removeAbandoned = this.removeAbandoned;
        }
        dataSource.setRemoveAbandoned(removeAbandoned);
        
        // removeAbandonedTimeout
        int removeAbandonedTimeout = ValueUtil.getInt(propsMap.get(PROP_KEY_REMOVEABANDONEDTIMEOUT), -1);
        if (-1 == removeAbandonedTimeout) {
            removeAbandonedTimeout = this.removeAbandonedTimeout;
        }
        dataSource.setRemoveAbandonedTimeout(removeAbandonedTimeout);
        
        // timeBetweenEvictionRunsMillis
        long timeBetweenEvictionRunsMillis = ValueUtil.getLong(propsMap.get(PROP_KEY_TIMEBETWEENEVICTIONRUNSMILLS),
                -1);
        if (-1 == timeBetweenEvictionRunsMillis) {
            timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis;
        }
        dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
        
        // minEvictableIdleTimeMillis
        long minEvictableIdleTimeMillis = ValueUtil.getLong(propsMap.get(PROP_KEY_MINEVICTABLEIDLETIMEMILLIS), -1);
        if (-1 == minEvictableIdleTimeMillis) {
            minEvictableIdleTimeMillis = this.minEvictableIdleTimeMillis;
        }
        dataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        
        // connectionProperties
        String connProperties = ValueUtil.getString(propsMap.get(PROP_KEY_CONNECTIONPROPERTIES), null);
        if (null == connProperties) {
            connProperties = this.connProperties;
        }
        if (StringUtils.isNotBlank(connProperties)) {
            dataSource.setConnectionProperties(connProperties);
        }
        
        return dataSource;
    }
    
    public void removeDataSource(String sourceName) throws SQLException {
        dsMapLock.lock();
        try {
            BasicDataSource ds = dsMap.remove(sourceName);
            if (null != ds) {
                ds.close();
            }
            if (null != dsPropsMap) {
                dsPropsMap.remove(sourceName);
            }
        } finally {
            dsMapLock.unlock();
        }
    }
    
    @On(eventName = "db" + HSBCSZooKeeperConfigurator.EVT_RELOAD_SUFFIX)
    public void refreshDataSource(String sourceName) throws SQLException {
        LOG.info("...reloading datasource of " + sourceName);
        dsMapLock.lock();
        try {
            removeDataSource(sourceName);
            getDataSource(sourceName);
        } finally {
            dsMapLock.unlock();
        }
        LOG.info("...reloaded datasource of " + sourceName);
    }
    
    protected abstract String getDataSourceKey();
    
    protected BasicDataSource getKeySpecifiedDataSource() {
        String key = getDataSourceKey();
        return getDataSource(key);
    }
    
    @Override
    public Connection getConnection() throws SQLException {
        return getKeySpecifiedDataSource().getConnection();
    }
    
    @Override
    public synchronized void close() throws SQLException {
        dsMapLock.lock();
        try {
            Iterator<Entry<String, BasicDataSource>> dsIt = dsMap.entrySet().iterator();
            while (dsIt.hasNext()) {
                Entry<String, BasicDataSource> dsEntry = dsIt.next();
                dsEntry.getValue().close();
                dsIt.remove();
            }
        } finally {
            dsMapLock.unlock();
        }
        super.close();
    }
    
    public boolean isRemoveAbandoned() {
        return removeAbandoned;
    }
    
    public void setRemoveAbandoned(boolean removeAbandoned) {
        this.removeAbandoned = removeAbandoned;
    }
    
    public int getRemoveAbandonedTimeout() {
        return removeAbandonedTimeout;
    }
    
    public void setRemoveAbandonedTimeout(int removeAbandonedTimeout) {
        this.removeAbandonedTimeout = removeAbandonedTimeout;
    }
    
    @Override
    public void setConnectionProperties(String connectionProperties) {
        connProperties = connectionProperties;
    }
    
}
